"""Implementation of a Threaded Modbus Server."""
from __future__ import annotations

import asyncio
import traceback

from pymodbus.constants import ExcCodes
from pymodbus.exceptions import ModbusIOException, NoSuchIdException
from pymodbus.logging import Log
from pymodbus.pdu import ExceptionResponse
from pymodbus.transaction import TransactionManager
from pymodbus.transport import CommParams


class ServerRequestHandler(TransactionManager):
    """Handle client connection."""

    def __init__(self, owner, trace_packet, trace_pdu, trace_connect):
        """Initialize."""
        params = CommParams(
            comm_name="server",
            comm_type=owner.comm_params.comm_type,
            reconnect_delay=0.0,
            reconnect_delay_max=0.0,
            timeout_connect=0.0,
            host=owner.comm_params.source_address[0],
            port=owner.comm_params.source_address[1],
            handle_local_echo=owner.comm_params.handle_local_echo,
        )
        self.server = owner
        self.framer = self.server.framer(self.server.decoder)
        self.running = False
        super().__init__(
            params,
            self.framer,
            0,
            True,
            trace_packet,
            trace_pdu,
            trace_connect,
        )

    def callback_disconnected(self, exc: Exception | None) -> None:
        """Call when connection is lost."""
        super().callback_disconnected(exc)
        if exc is None:
            Log.debug(
                "Handler for stream [{}] has been canceled", self.comm_params.comm_name
            )
        else:
            Log.debug(
                "Client Disconnection {} due to {}",
                self.comm_params.comm_name,
                exc,
            )
        self.running = False

    def callback_data(self, data: bytes, addr: tuple | None = None) -> int:
        """Handle received data."""
        try:
            used_len = super().callback_data(data, addr)
        except ModbusIOException:
            response = ExceptionResponse(
                40,
                exception_code=ExcCodes.ILLEGAL_FUNCTION
            )
            self.server_send(response, 0)
            return(len(data))
        if self.last_pdu:
            self.loop.call_soon(self.handle_later)
        return used_len

    def handle_later(self):
        """Change sync (async not allowed in call_soon) to async."""
        asyncio.run_coroutine_threadsafe(self.handle_request(), self.loop)

    async def handle_request(self):
        """Handle request."""
        if not self.last_pdu:
            return
        try:
            if self.server.broadcast_enable and not self.last_pdu.dev_id:
                # if broadcasting then execute on all device contexts,
                # note response will be ignored
                for dev_id in self.server.context.device_ids():
                    await self.last_pdu.update_datastore(self.server.context[dev_id])
                return

            context = self.server.context[self.last_pdu.dev_id]
            response = await self.last_pdu.update_datastore(context)

        except NoSuchIdException:
            if self.server.ignore_missing_devices:
                Log.debug("ignoring request for unknown device id: {}", self.last_pdu.dev_id)
                return  # the client will simply timeout waiting for a response
            Log.error("requested device id does not exist: {}", self.last_pdu.dev_id)
            response = ExceptionResponse(self.last_pdu.function_code, ExcCodes.GATEWAY_NO_RESPONSE)
        except Exception as exc:  # pylint: disable=broad-except
            Log.error(
                "Datastore unable to fulfill request: {}; {}",
                exc,
                traceback.format_exc(),
            )
            response = ExceptionResponse(self.last_pdu.function_code, ExcCodes.DEVICE_FAILURE)
        response.transaction_id = self.last_pdu.transaction_id
        response.dev_id = self.last_pdu.dev_id
        self.server_send(response, self.last_addr)

    def server_send(self, pdu, addr):
        """Send message."""
        if not pdu:
            Log.debug("Skipping sending response!!")
        else:
            self.pdu_send(pdu, addr=addr)
